Concepts


Author: Kimmy

我们先从整个 C++ 社区心念多年的特性说起。

首先我们来看 Concept 是什么。

泛型编程中,我们通常需要限制某个类型的行为,并通过这种方式来保证我们使用泛型的代码是正确的,不存在异常行为。

比如 Java 提供了泛型约束的方式,如果你没有在泛型参数列表中声明某类型需要满足的约束,那就只能把该类型的对象当成 Object 来用,或者使用 instanceof 和强制类型转换来得到你想要的方法。

这样的结果就是,是否使用泛型根本没啥区别

比如我们需要处理一个可迭代对象:

class Processor<T extends Iterable<?>> {
  void process(T t) {
    t.iterator() // !
  }
}

备注了 ! 的那行代码就通过我们在类型声明处的 extends Iterable<?> 约束来保证的。这样就不会出现编译期的类型异常。

不过这个操作其实也有很多问题。

而远在 Concept 还没进入标准之前,C++就已经能做到类似的操作了,只是过程有些复杂。

比如,我们要限制一个类型参数必须是可以默认构造的(即有一个公开的无参构造函数),要怎么做呢?

(以下代码是简化版,没有进行 remove_cv 或者 decay 的处理)

// 1
template<class T, class U = std::void_t<>>
struct IsDefaultConstructible: std::false_type {};

// 2
template<class T>
struct IsDefaultConstructible<T, std::void_t<decltype(T())>>: std::true_type {};

// 3
struct T {};
struct U { U(int) {}; };

// 4
static_assert(IsDefaultConstructible<T>::value == true);
static_assert(IsDefaultConstructible<U>::value == false);

// 5
template<class T, std::enable_if_t<IsDefaultConstructible<T>::value>* = nullptr>
struct RequiresDefaultConstructible {
  T x = T {};
};

// 6
RequiresDefaultConstructible<T> {};

// 7
RequiresDefaultConstructible<U> {};

这段代码会在// 7处编译失败,因为 U 只有一个带参构造函数,无法进行无参构造。

我们来分别解释一下这些代码干了啥

上面的操作充斥着 C++ 元编程技巧,远比声明一个接口就使用要复杂得多;并且熟悉的朋友应该已经看出来了,decltype 是 C++11 的,enable_if_t 是 C++14 的,void_t 是 C++17 的,在这些特性和工具库未出现之前的 C++03,想要实现就要更复杂很多。

并且,糟糕的是,即便在 2021 年的 gcc 11 编译器上,// 7行的编译错误信息都几乎跟 IsConstructible 毫无关系:

In substitution of 'template<bool _Cond, class _Tp> using enable_if_t = typename std::enable_if::type [with bool _Cond = false; _Tp = void]':
test.cpp:23:35:   required from here
error: no type named 'type' in 'struct std::enable_if<false, void>'
 2514 |     using enable_if_t = typename enable_if<_Cond, _Tp>::type;
      |           ^~~~~~~~~~~
test.cpp: In function 'int main()':
test.cpp:23:35: note: invalid template non-type parameter
   23 |     RequiresDefaultConstructible<U> {};
      |                                   ^

每一个调试 C++ 模版的人上辈子都是折翼的天使。

终于千呼万唤始出来的 Concept,解决了大部分相关的问题。在有了 Concept 的情况下,限制默认构造可以这样写了:

template<class T>
concept DefaultConstructible = requires {
    T {};
};

template<DefaultConstructible T>
struct RequiresDefaultConstructible {
    T x = T {};
};

这个时候当你再去实例化 RequiresDefaultConstructible<U>,一切都变得那么简单自然:

test.cpp:20:35: error: template constraint failure for 'template<class T>  requires  DefaultConstructible<T> struct RequiresDefaultConstructible'
   20 |     RequiresDefaultConstructible<U> {};
      |                                   ^
test.cpp:20:35: note: constraints not satisfied
test.cpp: In substitution of 'template<class T>  requires  DefaultConstructible<T> struct RequiresDefaultConstructible [with T = U]':
test.cpp:20:35:   required from here
test.cpp:5:9:   required for the satisfaction of 'DefaultConstructible<T>' [with T = U]
test.cpp:5:32:   in requirements  [with T = U]
test.cpp:6:5: note: the required expression 'T{}' is invalid
    6 |     T {};
      |     ^~~~

更重要的是,Concept 的验证和模版的展开完全是编译期的,requires 块里你可以写各种可能的验证,并且 Concept 本身就是个 bool 类型的结果,所以可以结合之前几乎所有的 C++ type traits 一起用。

即便是拖了这么久才实现出来,这项特性依然是能够吊打绝大多数编程语言的泛型约束功能。

至于 Concept 这项功能到底有什么用呢?我们到后面讲到 Ranges 的时候再细说。

创建时间:2021-08-12 最近更新时间:2023-11-03